import { Injectable } from '@angular/core';
import { filter, map, Subject, Subscription } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class EventBusService {

  private eventBus: Subject<any> = new Subject();

  private eventMap: Map<Subscription, string> = new Map<Subscription, string>();

  private keyMatch(key: string, wildcard: string) {
    const w = '*';
    const ww = '**';
    const partMatch = (wl: string, k: string) => wl === w || wl === k;
    const sep = ':';
    const kArr = key.split(sep);
    const wArr = wildcard.split(sep);
    const kLen = kArr.length;
    const wLen = wArr.length;
    if (wLen > kLen) {
      return false;
    }
    for (let i = 0; i < kLen; i++) {
      const cK = kArr[i];
      const cW = wArr[i];
      if (cW === ww && typeof cK !== 'undefined') {
        return true;
      }
      if (!partMatch(cW, cK)) {
        return false;
      }
    }
    return true;
  }

  cast(key: string, data?: any): void {
    if (!key.trim().length) {
      throw new Error('key parameter must be a string and must not be empty');
    }
    const metadata = new MetaData(key, data);
    this.eventBus.next({ key, data, metadata });
  }

  on<T>(key: string, observer?: (data: any) => void): Subscription {
    const subscription = this.eventBus.asObservable().pipe(filter((event) => this.keyMatch(event.key, key)), map((event) => event.metadata.data)).subscribe(observer);
    this.eventMap.set(subscription, key);
    return subscription;
  }

  off(subscription: Subscription): void {
    if (!this.eventMap.has(subscription)) {
      return;
    }
    subscription.unsubscribe();
    this.eventMap.delete(subscription);
  }

  offAll(): void {
    this.eventMap.forEach((mapKey, value) => {
      this.off(value);
    });
  }

  offByKey(key: string): void {
    this.eventMap.forEach((mapKey, value) => {
      if (!this.keyMatch(mapKey, key)) {
        return;
      }
      this.off(value);
    });
  }

  havaIsKey(key: string): boolean {
    let bool = false;
    this.eventMap.forEach((mapKey, value) => {
      if (this.keyMatch(mapKey, key)) {
        bool = true;
      }
    });
    return bool;
  }
}

class MetaData {

  private readonly _id;

  private readonly _key;

  private readonly _data;

  private readonly _timestamp;

  /**
   * Constructor for this class.
   *
   * @param key Original key associated to the message sent through the events bus.
   * @param [data] Optional: Additional data sent with the message.
   */
  constructor(key: string, data: any) {
    this._id = this.uuid();
    this._key = key;
    this._data = data;
    this._timestamp = new Date().getTime();
  }

  /**
   * Gets unique identifier of the message sent through the events bus.
   */
  get id() {
    return this._id;
  }

  /**
   * Original key associated to the message.
   */
  get key() {
    return this._key;
  }

  /**
   * Data associated to message. It's optional.
   */
  get data() {
    return this._data;
  }

  /**
   * Gets the time in milliseconds in which the message was generated.
   */
  get timestamp() {
    return this._timestamp;
  }

  /**
   * Generates UUID version 4. The solution above uses Math.random() for brevity, however Math.random() is not
   * guaranteed to be a high-quality RNG.
   *
   * @return UUID version 4.
   */
  uuid() {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
      // eslint-disable-next-line no-bitwise
      const r = (Math.random() * 16) | 0;
      // eslint-disable-next-line no-bitwise
      const v = c === 'x' ? r : (r & 0x3) | 0x8;
      return v.toString(16);
    });
  }
}
